package org.swift.commons.deployer.framework.schema;

import java.util.Hashtable;
import java.util.Iterator;
import java.util.Map;

import javax.xml.namespace.QName;

import org.apache.axis2.description.AxisOperation;
import org.apache.axis2.description.AxisService;
import org.apache.axis2.description.java2wsdl.Java2WSDLConstants;
import org.apache.axis2.description.java2wsdl.TypeTable;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.ws.commons.schema.XmlSchema;
import org.apache.ws.commons.schema.XmlSchemaCollection;
import org.apache.ws.commons.schema.XmlSchemaComplexType;
import org.apache.ws.commons.schema.XmlSchemaElement;
import org.apache.ws.commons.schema.XmlSchemaForm;
import org.apache.ws.commons.schema.XmlSchemaObject;
import org.apache.ws.commons.schema.utils.NamespaceMap;

@SuppressWarnings("unchecked")
public abstract class SchemaGenerator {
	private static Log log = LogFactory.getLog(SchemaGenerator.class);

	protected String attrFormDefault = null;
	protected String elementFormDefault = null;
	protected Map<String, XmlSchema> schemaMap = new Hashtable<String, XmlSchema>();
	protected XmlSchemaCollection xmlSchemaCollection = new XmlSchemaCollection();
	protected TypeTable typeTable = new TypeTable();

	public void generateSchema(AxisService axisService) {
		log.debug("Creating schema for service " + axisService.getName());
		Iterator<AxisOperation> operations = axisService.getOperations();
		while (operations.hasNext()) {
			generateSchemaforOperation(operations.next());
		}
		axisService.addSchema(this.schemaMap.values());
	}

	protected abstract void generateSchemaforOperation(
			AxisOperation axisOperation);

	/**
	 * Create an Xml Schema with the given namespace
	 * 
	 * @param targetNamespace namespace to be used for schema
	 * @return new schema
	 */
	protected XmlSchema getXmlSchema(String targetNamespace) {
		XmlSchema xmlSchema;
		if ((xmlSchema = schemaMap.get(targetNamespace)) == null) {
			xmlSchema = new XmlSchema(targetNamespace, xmlSchemaCollection);
			xmlSchema.setAttributeFormDefault(getAttrFormDefaultSetting());
			xmlSchema.setElementFormDefault(getElementFormDefaultSetting());
			schemaMap.put(targetNamespace, xmlSchema);
			initializeNamespaceContext(xmlSchema);
		}
		return xmlSchema;
	}

	protected void initializeNamespaceContext(XmlSchema xmlSchema) {
		NamespaceMap prefixmap = new NamespaceMap();
		prefixmap.put("xsd",Java2WSDLConstants.URI_2001_SCHEMA_XSD);
		xmlSchema.setNamespaceContext(prefixmap);
	}

	protected XmlSchemaForm getAttrFormDefaultSetting() {
		if (Java2WSDLConstants.FORM_DEFAULT_QUALIFIED
				.equals(getAttrFormDefault())) {
			return new XmlSchemaForm(XmlSchemaForm.QUALIFIED);
		} else {
			return new XmlSchemaForm(XmlSchemaForm.UNQUALIFIED);
		}
	}

	protected XmlSchemaForm getElementFormDefaultSetting() {
		if (Java2WSDLConstants.FORM_DEFAULT_UNQUALIFIED
				.equals(getElementFormDefault())) {
			return new XmlSchemaForm(XmlSchemaForm.UNQUALIFIED);
		} else {
			return new XmlSchemaForm(XmlSchemaForm.QUALIFIED);
		}
	}

	/**
	 * Generate a complex type for output
	 * 
	 * @param operationName axis operation
	 * @param elName name of element complex type is bound to
	 * @param addComplextype create a top level type
	 * @return complex type
	 */
	protected XmlSchemaComplexType createSchemaTypeForMethodPart(
			String operationName, String elName, boolean addComplextype) {
		return createSchemaTypeForMethodPart(operationName, elName,
				addComplextype, true);
	}

	/**
	 * Generate a complex type for output
	 * 
	 * @param operationName axis operation
	 * @param elName name of element complex type is bound to
	 * @param addComplextype create a top level type
	 * @return complex type
	 */

	/**
	 * Generates a complex schema type and optionally adds it to the schema type
	 * in the wsdl, and optionally binding it to the given operation name
	 * 
	 * @param operationName the name of the axis operation to create a complex
	 *        type for
	 * @param elName the name of the element to inject into the schema
	 * @param addComplextype whether or not to add this as a complex type to the
	 *        schema type
	 * @param alsoRegisterToOperation whether or not to associate this complex
	 *        type as the operations return type
	 * @return
	 */
	protected XmlSchemaComplexType createSchemaTypeForMethodPart(
			String operationName, String elName, boolean addComplextype,
			boolean alsoRegisterToOperation) {
		String namespace = getDefaultNamespace();
		XmlSchema xmlSchema = getXmlSchema(namespace);
		QName elementName = new QName(namespace, elName,
				getDefaultNamespacePrefix());
		XmlSchemaComplexType complexType = getComplexTypeForElement(xmlSchema,
				elementName);
		if (complexType == null) {
			complexType = new XmlSchemaComplexType(xmlSchema);

			if (addComplextype) {
				XmlSchemaElement globalElement = new XmlSchemaElement();
				globalElement.setName(elName);
				globalElement.setQName(elementName);

				globalElement.setSchemaType(complexType);
				xmlSchema.getItems().add(globalElement);
				xmlSchema.getElements().add(elementName, globalElement);
			}
		}
		if (addComplextype && alsoRegisterToOperation) {
			typeTable.addComplexSchema(operationName, elementName);
		}
		return complexType;
	}

	protected XmlSchemaComplexType getComplexTypeForElement(QName name) {
		String uri = name.getNamespaceURI();
		if (uri == null)
			uri = getDefaultNamespace();
		if (uri != null) {
			XmlSchema complexTypeSchema = getXmlSchema(uri);
			if (complexTypeSchema != null) {
				return getComplexTypeForElement(complexTypeSchema, name);
			}
		}
		return null;
	}

	protected XmlSchemaComplexType createSchemaType(QName name) {
		XmlSchemaComplexType complexType = getComplexTypeForElement(name);
		if (complexType == null && (name.getNamespaceURI() != null)) {
			XmlSchema xmlSchema = getXmlSchema(name);
			complexType = new XmlSchemaComplexType(xmlSchema);
			complexType.setName(name.getLocalPart());
			xmlSchema.addType(complexType);
			xmlSchema.getItems().add(complexType);
		}
		return complexType;
	}

	private XmlSchema getXmlSchema(QName forElement) {
		return getXmlSchema(forElement.getNamespaceURI());
	}

	/**
	 * Search given schema for existing complex type of given qname
	 * 
	 * @param xmlSchema XmlSchema to search
	 * @param name Qname to look for
	 * @return complex type if exists or null
	 */
	protected XmlSchemaComplexType getComplexTypeForElement(
			XmlSchema xmlSchema, QName name) {
		Iterator<XmlSchemaObject> iterator = xmlSchema.getItems().getIterator();
		while (iterator.hasNext()) {
			XmlSchemaObject object = iterator.next();
			if (object instanceof XmlSchemaElement
					&& ((XmlSchemaElement) object).getQName().equals(name)) {
				return (XmlSchemaComplexType) ((XmlSchemaElement) object)
						.getSchemaType();
			} else if (object instanceof XmlSchemaComplexType
					&& ((XmlSchemaComplexType) object).getQName().equals(name)) {
				// return (XmlSchemaComplexType) ((XmlSchemaElement)
				// object).getSchemaType();
				return (XmlSchemaComplexType) object;
			}
		}
		return null;
	}

	protected abstract String getDefaultNamespace();

	protected abstract String getDefaultNamespacePrefix();

	public String getAttrFormDefault() {
		return attrFormDefault;
	}

	public void setAttrFormDefault(String attrFormDefault) {
		this.attrFormDefault = attrFormDefault;
	}

	public String getElementFormDefault() {
		return elementFormDefault;
	}

	public void setElementFormDefault(String elementFormDefault) {
		this.elementFormDefault = elementFormDefault;
	}
}
